﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Cloak.Linq;

namespace Cloak.Reflection
{
	/// <summary>
	/// Strongly-typed invocation using delegates compiled by the <see cref="System.Linq.Expressions"/> namespace
	/// </summary>
	public static class Invoke
	{
		/// <summary>
		/// Compiles and invokes the lambda expression described by the specified tree and which
		/// returns the specified type
		/// </summary>
		public static TResult Tree<TResult>(Expression tree)
		{
			Contract.Requires(tree != null);

			var invoke = Expression.Lambda<Func<TResult>>(tree).Compile();

			return invoke();
		}

		#region Func
		/// <summary>
		/// Invokes the specified static method by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(MethodInfo method, params object[] arguments)
		{
			return FuncWith<TResult>(method, arguments.ToConstants().ToArray());
		}

		/// <summary>
		/// Invokes the specified method by compiling and invoking a delegate which returns the
		/// specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(object instance, MethodInfo method, params object[] arguments)
		{
			return FuncWith<TResult>(
				Expression.Constant(instance),
				method,
				arguments.ToConstants().ToArray());
		}

		/// <summary>
		/// Invokes the specified static method by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(
			Type type,
			string methodName,
			Type[] typeArguments,
			params object[] arguments)
		{
			return FuncWith<TResult>(
				type,
				methodName,
				typeArguments,
				arguments.ToConstants().ToArray());
		}

		/// <summary>
		/// Invokes the specified method by compiling and invoking a delegate which returns the
		/// specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(
			object instance,
			string methodName,
			Type[] typeArguments,
			params object[] arguments)
		{
			return FuncWith<TResult>(
				Expression.Constant(instance),
				methodName,
				typeArguments,
				arguments.ToConstants().ToArray());
		}

		/// <summary>
		/// Invokes the specified static method by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(
			Type type,
			string methodName,
			Type typeArgument,
			params object[] arguments)
		{
			return Func<TResult>(type, methodName, new Type[] { typeArgument }, arguments);
		}

		/// <summary>
		/// Invokes the specified method by compiling and invoking a delegate which returns the
		/// specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Func<TResult>(
			object instance,
			string methodName,
			Type typeArgument,
			params object[] arguments)
		{
			return Func<TResult>(instance, methodName, new Type[] { typeArgument }, arguments);
		}
		#endregion

		#region FuncWith

		// TODO: These shouldn't have to be named differently. Overload resolution should suffice.

		/// <summary>
		/// Invokes the specified static method by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult FuncWith<TResult>(MethodInfo method, params Expression[] arguments)
		{
			return Tree<TResult>(Expression.Call(method, arguments));
		}

		/// <summary>
		/// Invokes the specified method by compiling and invoking a delegate which returns the
		/// specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult FuncWith<TResult>(Expression instance, MethodInfo method, params Expression[] arguments)
		{
			return Tree<TResult>(Expression.Call(instance, method, arguments));
		}

		/// <summary>
		/// Invokes the specified static method by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult FuncWith<TResult>(
			Type type,
			string methodName,
			Type[] typeArguments,
			params Expression[] arguments)
		{
			return Tree<TResult>(Expression.Call(type, methodName, typeArguments, arguments));
		}

		/// <summary>
		/// Invokes the specified method by compiling and invoking a delegate which returns the
		/// specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult FuncWith<TResult>(
			Expression instance,
			string methodName,
			Type[] typeArguments,
			params Expression[] arguments)
		{
			return Tree<TResult>(Expression.Call(instance, methodName, typeArguments));
		}
		#endregion

		// TODO: Action

		// TODO: ActionWith

		#region Member
		/// <summary>
		/// Invokes the specified static member access by compiling and invoking a delegate
		/// which returns the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Member<TResult>(MemberInfo member)
		{
			if(member == null)
			{
				throw new ArgumentNullException("member");
			}

			return Member<TResult>(member.DeclaringType, member);
		}

		/// <summary>
		/// Invokes the specified member access by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Member<TResult>(object instance, MemberInfo member)
		{
			return MemberWith<TResult>(Expression.Constant(instance), member);
		}

		/// <summary>
		/// Invokes the specified member access by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult MemberWith<TResult>(Expression instance, MemberInfo member)
		{
			return Tree<TResult>(Expression.MakeMemberAccess(instance, member));
		}
		#endregion

		#region Constructor
		/// <summary>
		/// Invokes the specified constructor by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult Constructor<TResult>(ConstructorInfo constructor, params object[] arguments)
		{
			if(constructor == null)
			{
				throw new ArgumentNullException("constructor");
			}

			if(arguments == null)
			{
				throw new ArgumentNullException("arguments");
			}

			return ConstructorWith<TResult>(constructor, arguments.ToConstants().ToArray());
		}

		/// <summary>
		/// Invokes the specified constructor by compiling and invoking a delegate which returns
		/// the specified type
		/// </summary>
		/// <exception cref="ArgumentNullException"/>
		/// <exception cref="ArgumentException"/>
		public static TResult ConstructorWith<TResult>(ConstructorInfo constructor, params Expression[] arguments)
		{
			if(constructor == null)
			{
				throw new ArgumentNullException("constructor");
			}

			if(arguments == null)
			{
				throw new ArgumentNullException("arguments");
			}

			return Tree<TResult>(Expression.New(constructor, arguments));
		}
		#endregion
	}
}